avatar

目录
325 - 类内成员初始化

类内成员初始化

🧑‍💻 User

In this lecture we’re going to look at in class member initialization.And this is a facility we have in C plus plus to control how member variables are initialized when wedeclare them directly inside the body of our class.So suppose this is a private section of some class you are declaring and you can set up member variableslike this without doing any initialization.So what this is going to do by default, it’s going to initialize your data to contain some junk data.It might be zero, it might be really anything that can fit in this type that we have here.So we don’t have control on what is going to be in here.But you might also explicitly put in your own data.For example, you can go through assignment and put in zero and make sure that the member variablesare going to contain zero inside.When you create an object of your class and this is going to work.Another option we have is to use brace initialization, and this is going to implicitly initialize yourmember variables to zero.So this is going to be exactly the same thing like this, but it’s going to be implicitly done by thecompiler When it sees brace initialization like this.You can also explicitly put in your data, just like we do here, using brace initialization.And this is going to put in zero.And you can even initialize custom types that you set up yourself.And this is going to call your own default constructor to construct your thanks.And so you can also use in class member initialization to set up and initialize classes of your owntypes.For example, here we have an integer type that we have built ourselves and we are setting up a membervariable of that type.And if we use brace initialization like this, this is going to call our default constructor and you’regoing to see that this is going to work exactly like you want.But one thing I want you to know is that it wasn’t always this easy to do in class member initializationbecause before a C plus plus 11, the list of types for which you could do in class member initializationwas really limited before C plus plus 11.You could only do this for static constants of integral types.So integer size T and things like that.And we could only do this only for static constants of enum type.If you do this for any other type that is not covered by what we see here, you would get a compilererror, but we are using modern C plus plus so we can really do in class member initialization for anytype and it is going to work just like you see here.So let’s head over to Visual Studio code and play with this a little more.Okay.Here we are in our working folder.The current project is in class member initialization.We’re going to grab our template files pretty quick.We’re going to put them in place and we’re going to open this in Visual Studio code by dragging anddropping here, this is going to open our files.We have the main CPP file.We can clean this up a little bit.And before we do anything, I want to bring in a class that we’re going to use to show that in classmember initialization can also work for our own custom types.We’re going to put in an integer class.So let’s put a header file for that here and we’re going to do a CPP file.We are going to put in the header file.Nothing complicated here.We have our include guard, we have the class, we have a member variable inside and we have a set ofsetters and getters and we have a constructor and destructor here.Nothing complicated.Okay?Now that we have this, we can put in the CPP file.Let’s do that.And we are just going to put in the implementations for our constructors.We have a constructor that takes one parameter and it’s going to use that to initialize our member variable,which is called inner.And if we go here, we’re going to see that now that we have the integer class, we can go in Main.cppand really try to play with these things.What we’re going to do is include the integer class and we are going to set up a class called Point.We already know a class like this, so we’re going to do this, we’re going to call this point.And you see Visual Studio code is going to autocomplete these things.So we have I don’t like this kinds of autocomplete because they really make things hard to see.And we’re going to put in our own class called Point.Let’s do that and we’re going to put our closing semicolon.We’re going to put in a private section and we’re going to have a public section on top.And inside we’re going to have two member variables double X and double Y, and we’re going to alignthese things a little bit better.So let’s do that.And we have our class.So this is what we want.Let’s make sure that we have a constructor.That we can use to create point objects and let’s call our members M, x and M y.I think this is going to work and we’re going to say point.We’re going to say double X here and double Y, and we are going to use an initializer list to put thesevalues in.So we’re going to say M X is going to take x, n and m, y is going to take y, n.We’re going to use brace initialization here and we’re going to put in our body and we have our constructorhere.But what we want is to learn about in class member initialization here and we have already done that.If we do something like this, we want to be putting any initialization at all and this is really goingto put in junk data in our member variables.They are not going to be initialized and we’re going to have a problem to be able to try this out.Let’s really put in a default constructor.We’re going to say point and we’re going to say default.And this is not going to do anything.It’s just going to put an empty body on the constructor and it’s not going to do any initializationon our point member variables here.So we can set up a function which is going to print the point object and we’re going to be able to seethis member variables.But if you want, you can also use a debugger to see that let’s use a debugger.We haven’t used the debugger in a while, so let’s do that.We’re going to create a point object and we’re going to say P one.We’re not going to put any parentheses here.This is going to call the default constructor here and we should have junk data inside m, x and m yfor this P one point here.Okay, let’s put a break point here and we are going to use a debugger.So we’re going to use the debugging tools that come with GCC.We’re going to start debugging and this is going to build our binary.The binary is going to be passed into the debugger and we’re going to hit the breakpoint here.We have hit the breakpoint.And if we look at our locals, we can see what we have in M, X and m y.You can see that it’s data that we really never put in our object here.And what this is going to do, it’s going to see the syntax we have on line 21.It is going to look at the default constructor that we have here, and the default constructor is reallynot going to do anything.It’s going to be an empty body constructor and I really want you to save this.So let’s not put in a default constructor like this and put in a body of our own and we’re going tosay STD and we’re not going to put anything in the body.So the member variables are going to be left the way they are, and we’re going to be able to see thatthrough the debugger that we have junk data inside our member variables here.When we hit the breakpoint on line 22 here, I hope you understand this.So we’re going to debug again.We’re going to run a debugging session.This is going to build our program.The build is going to be good.We’re going to pass the executable through the debugger and we’re going to hit the breakpoint here.So when we do that, we’re going to step over to be able to hit the return statement here.And at this point, the point object will be already constructed and we will see what is inside.You see, X has some junk data, Y has some junk data.And this is what default initialization like this is going to do.You are not putting in any initialization at all.So the compiler is going to put in some junk data.And this is one way we can do this, but we are using modern C plus plus.We really have many ways we can control how our member variables are initialized and we’re going touse assignment to do our initialization.So the way we can do that, we can really copy what we have on top here and use assignment.Or to really be super clear, we’re going to say explicit assignment.Okay, so if we do this, we can, for example, say 0.1 for X and say 0.1 for Y.We can say that and if we do this, we’re going to see that.Now, the data is not going to be junk.It’s not going to be junk data like we see in the debugger here in our locals.We’re going to see the data we explicitly put in through our assignment operator here.We’re going to kill the debugging session and we’re going to run it again so that the changes we havehere are taken into consideration.The binary is going to be generated.It’s going to be passed it to the debugger and we’re going to hit the breakpoint at line 32 here.If we look at our locals, you see now that we have the data we put in, we have 0.1, 0.1 and it iswhat we put in here.And this is really cool.So this is another way you can control how your in class member initialization works.We’re going to comment this out because we still have a few ways we need to see to do this.We can also use empty based initializers.Let’s do that.And the way we do that, we say double m x and we’re going to put an.Pair of curly braces and we’re going to say double m X and an empty pair of curly braces.And this is going to default.Initialize whatever you have here, because this is a fundamental type.This is going to initialize this to zero.But if it was a custom type like integer, for example, this would call the default constructor foryour type.So let’s try and debug again and see that this is going to initialize our member variables to zero,which is a zero initialization of the double type that we have here.So we’re going to start a debugging session again and it seems like we have a compiler error.What is the problem here?Double m y.We are initializing this twice and we’re going to get a compiler error.Let’s try and build and make sure that we have no compiler error.So we are going to start a debugging session again and we have an error here.Let’s close this and if we go here, it says class point does not have any field named M Why are wenamed this?Mm So let’s say m y here and let’s build it to make sure this is not giving us any more compiler errors.The build is good.We’re going to start a debugging session and we should hit our breakpoint here.And if we go and we’re going to see that our member variables were initialized to zero as the resultof this braced initializer, we have in our member variable declaration here.So this is really cool.So you can do this, but you can even explicitly put a value in this braced initializer and it is goingto pick the value up and put that in your member variable.So for example, if we change this to 0.1 and put that in here, this is going to work and this is nolonger empty braced initializer, let’s just say braced initializer.And if we kill the session here and run it again, we’re going to see that our member variables havebeen initialized to 0.1.And if we look at our locals, this is exactly what we expect here.So this is really cool.You can do all these kinds of crazy things, but remember, we have a custom type which is called integerhere, and we can even use this in our in class member initialization to show you that this is actuallygoing to work.So what we’re going to do is put in a default constructor and give it a body.We want to note that this was called, so we’re going to say Stdcout default constructor called Okay.So if we do this and then come in our point class and set up an integer member variable, let’s forexample, say integer and say I and I used a braced initializer here.This is going to call the constructor of the integer class and we should see this message here printedout on the console.If we’re trying to set up an object of point type in our main function here.So we’re going to go in main here and make sure that we have this created.This is it.So let’s build and try to run the application and we should see a call to the constructor of the integerclass as a result of this member variable that we have here, which is used brace initialization.This is going to default constructor to the object.So let’s build and try to run this.We’re going to use GCC to build.You’re going to see that the world is good, so this is fine.We can clear and run roster and you see default constructor called but the const constructor for whatThe default constructor for integer.Let’s say that we’re going to build the world should be good and if we run roster we should see thedefault constructor called If you want you can set up a breakpoint in the integer body here and you’regoing to see it hit by the debugger.But the main message is that you can really do in class member initialization for any type, be it afundamental type like a double.For example, here we have the data which is not initialized and the compiler is just going to put injunk data.You can use assignment, you can use Brace Initializers, you can even in class member initialize customtypes like we have the integer class here and this is going to work as you expect.This is going to call your default constructor and it is going to do whatever you are doing in yourdefault constructor.In this case, we really aren’t doing anything.We are just putting a debug statement to know that the constructor was called here.This is really all we set out to do in this lecture.I hope you found it interesting.We are going to stop here in this one.The next one we’re going to try and recap what we saw in this chapter.Go ahead and finish up here and meet me there.


🤖 Assistant

好的,很高兴能为您深入讲解C++中的“类内成员初始化”([in-class member initialization] 或 ICMI)。作为您C++学习之旅的向导,我将尽力用最清晰、最全面的方式来帮助您理解这个概念。


C++ 类内成员初始化深度解析

简介: 本节课将带您全面了解C++11引入的类内成员初始化(ICMI)机制,它允许我们直接在类定义中为成员变量设定默认初始值,从而有效避免“垃圾数据”并简化代码。


1. 为什么需要类内成员初始化?

在C++中,当我们声明一个类(class)时,它通常包含一些成员变量(member variables)。这些成员变量在类的对象被创建时需要被初始化。如果不对它们进行显式初始化,它们可能会包含一些不确定的值,我们称之为“垃圾数据”([junk data])。

问题:
假设您有一个Point类,里面有两个double类型的成员变量m_xm_y,用于表示点的坐标:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Point {
private:
double m_x;
double m_y;
public:
// 默认构造函数
Point() {
// 如果这里不显式初始化 m_x 和 m_y
// 它们将包含不确定的“垃圾数据”
// 例如:m_x = 随机值, m_y = 随机值
}

// 其他构造函数或成员函数
};

// 在 main 函数中创建对象
int main() {
Point p1; // 调用默认构造函数,m_x 和 m_y 未初始化
// 此时访问 p1.m_x 和 p1.m_y 是危险的,因为它们是垃圾数据
return 0;
}

在这种情况下,当您创建Point对象p1时,p1.m_xp1.m_y的值是未知的,它们可能存储着之前内存中的任意数据。这被称为默认初始化([default initialization]),对于内置类型(如int, double等),默认初始化不会进行任何操作,导致“垃圾数据”。这通常会导致程序行为不确定或崩溃。

解决方案: C++11引入了类内成员初始化([in-class member initialization],简称ICMI)这一强大特性。它允许您在声明成员变量的同时,直接在类定义内部为其指定一个默认的初始值。当您创建类的对象,并且没有在构造函数的成员初始化列表([member initializer list])中显式初始化该成员时,ICMI就会生效。

2. 类内成员初始化的多种方式

类内成员初始化提供了几种语法来设置默认值:

2.1. 赋值初始化 ([Assignment Initialization])

这是最直观的方式,使用赋值运算符=来指定初始值。

示例:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point {
private:
double m_x = 0.0; // 使用赋值初始化
double m_y = 0.0; // m_x 和 m_y 默认都初始化为 0.0
public:
Point() {
// 当创建 Point 对象时,如果构造函数不显式初始化 m_x 和 m_y
// 它们将默认被初始化为 0.0
}
};

int main() {
Point p1; // 调用 Point() 构造函数,m_x 和 m_y 会是 0.0
// 您可以打印 p1.m_x 和 p1.m_y 来验证
return 0;
}

解释: 在这里,m_xm_y将始终以0.0作为它们的默认值。如果您创建Point对象时,没有在构造函数中明确为m_xm_y提供初始值,那么它们就会被这个类内初始化值覆盖。

2.2. 花括号初始化 ([Brace Initialization])

花括号初始化({})是C++11引入的一种通用初始化语法,它可以用于多种场景,包括类内成员初始化。

2.2.1. 空花括号 {} (零初始化或调用默认构造函数)
当您使用空花括号{}进行类内成员初始化时:

  • 对于内置类型([built-in types],如int, double, 指针等),这会进行零初始化([zero-initialization]),即将它们初始化为0(或等效的空值,如nullptr对于指针)。
  • 对于自定义类型([custom types],即您自己定义的类或结构体),这会尝试调用它们的默认构造函数([default constructor])。

示例:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 假设我们有一个自定义的 Integer 类
class Integer {
private:
int m_value;
public:
// 默认构造函数
Integer() {
m_value = 0; // 内部初始化为 0
std::cout << "Integer 默认构造函数被调用!" << std::endl;
}
// 带参数的构造函数
Integer(int val) : m_value(val) {
std::cout << "Integer 带参数构造函数被调用! 值: " << m_value << std::endl;
}

int getValue() const { return m_value; }
};

class Point {
private:
double m_x{}; // 零初始化,m_x 默认为 0.0
double m_y{}; // 零初始化,m_y 默认为 0.0
Integer m_id{}; // 调用 Integer 的默认构造函数
public:
Point() {
// 即使构造函数体为空,m_x, m_y, m_id 也会被上面的类内初始化规则初始化
}
// 带参数的构造函数,显式初始化 m_x 和 m_y
Point(double x_in, double y_in) : m_x{x_in}, m_y{y_in} {
// 注意:m_id 仍然会调用其默认构造函数,因为这里没有显式初始化它
}
void print() const {
std::cout << "Point: (" << m_x << ", " << m_y << "), ID: " << m_id.getValue() << std::endl;
}
};

int main() {
Point p1; // 调用 Point() 默认构造函数
// m_x = 0.0, m_y = 0.0
// m_id 调用 Integer 的默认构造函数
p1.print();

std::cout << "---" << std::endl;

Point p2(10.5, 20.5); // 调用 Point(double, double) 构造函数
// m_x = 10.5, m_y = 20.5
// m_id 仍然调用 Integer 的默认构造函数
p2.print();
return 0;
}

输出:
Code
1
2
3
4
5
Integer 默认构造函数被调用!
Point: (0, 0), ID: 0
---
Integer 默认构造函数被调用!
Point: (10.5, 20.5), ID: 0

解释:

  • double m_x{};double m_y{}; 会将m_xm_y初始化为0.0
  • Integer m_id{}; 会调用Integer类的默认构造函数,所以您会看到”Integer 默认构造函数被调用!”的输出,并且m_id.m_value被设置为0

2.2.2. 带值的花括号 {value} (值初始化)
您也可以在花括号中指定一个具体的值。

示例:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point {
private:
double m_x{1.0}; // 值初始化,m_x 默认为 1.0
double m_y{2.0}; // 值初始化,m_y 默认为 2.0
public:
Point() {} // 默认构造函数,m_x 和 m_y 会被类内初始化
void print() const {
std::cout << "Point: (" << m_x << ", " << m_y << ")" << std::endl;
}
};

int main() {
Point p1; // 调用 Point() 默认构造函数
// m_x = 1.0, m_y = 2.0
p1.print();
return 0;
}

输出:
Code
1
Point: (1, 2)

解释: m_xm_y将分别被初始化为1.02.0

3. 类内成员初始化与构造函数初始化列表的关系 (重要概念!)

这是一个非常关键且常被新手误解的地方。

  • 规则: 如果一个成员变量在类定义中通过ICMI被初始化,并且同时也在构造函数的成员初始化列表([member initializer list])中被显式初始化,那么构造函数初始化列表的初始化将优先,ICMI的初始化将被忽略。

  • 执行顺序:

    1. 当一个对象被创建时,首先会为所有成员变量(包括那些有ICMI的)分配内存。
    2. 对于那些有ICMI的成员,如果该构造函数的成员初始化列表没有显式地初始化它们,那么ICMI就会执行。
    3. 如果该构造函数的成员初始化列表显式地初始化它们,那么ICMI会被跳过,转而使用成员初始化列表的值。
    4. 最后,构造函数体内的代码才会被执行。

示例:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

class Point {
private:
double m_x = 0.0; // 类内初始化为 0.0
double m_y{0.0}; // 类内初始化为 0.0

public:
// 默认构造函数:没有显式初始化 m_x 和 m_y
Point() {
std::cout << "默认构造函数被调用" << std::endl;
}

// 带参数的构造函数:显式初始化 m_x 和 m_y
Point(double x_in, double y_in) : m_x(x_in), m_y(y_in) { // 注意这里是成员初始化列表
std::cout << "带参数构造函数被调用" << std::endl;
}

void print() const {
std::cout << "Point: (" << m_x << ", " << m_y << ")" << std::endl;
}
};

int main() {
Point p1; // 调用默认构造函数
// m_x 和 m_y 将使用类内初始化值 (0.0)
p1.print();

std::cout << "---" << std::endl;

Point p2(10.5, 20.5); // 调用带参数构造函数
// m_x 和 m_y 将使用构造函数初始化列表的值 (10.5, 20.5)
// 忽略类内初始化值
p2.print();

return 0;
}

输出:
Code
1
2
3
4
5
默认构造函数被调用
Point: (0, 0)
---
带参数构造函数被调用
Point: (10.5, 20.5)

结论: ICMI提供了一个默认值。只有当构造函数不显式提供成员的初始化值时,才会使用这个默认值。这使得您可以避免为每个构造函数都重复初始化相同的成员,从而减少代码冗余。

4. C++11 之前的限制 (历史背景)

在C++11之前,类内成员初始化功能非常有限。您只能对以下类型的成员进行类内初始化:

  • 静态常量整型([static constant integral types]):如 static const int, static const long
  • 静态常量枚举类型([static constant enum types])。

对于其他任何类型(包括非static成员变量,以及非const或非整型的static成员),您都无法进行类内初始化。试图这样做会导致编译错误([compiler error])。

示例 (C++11 之前不可行):

cpp
1
2
3
4
5
6
// C++11 之前会报错
class MyClass_Old {
int value = 0; // 错误!
double price{10.0}; // 错误!
static const int MAX_VAL = 100; // C++11 之前允许
};

幸运的是,我们现在使用的是现代C++,所以这个限制已经解除,您可以使用ICMI来初始化任何类型的非静态成员变量。

5. 总结与最佳实践

  • 优点:
    • 避免垃圾数据: 确保成员变量始终有一个已知的初始状态。
    • 代码简洁: 减少构造函数中重复的初始化代码。
    • 默认行为: 提供一个成员的默认初始值,除非被构造函数显式覆盖。
  • 适用场景: 当您希望为类中的某个成员变量提供一个普遍的默认初始值时,ICMI是最佳选择。
  • 注意事项: ICMI只适用于非静态数据成员([non-static data members])。静态数据成员的初始化规则不同,它们需要在类定义外部进行定义和初始化。

6. QA 闪卡 (Flash Cards)

  1. Q: 什么是“类内成员初始化”(ICMI)?
    A: 它是C++11引入的一种特性,允许直接在类定义内部为非静态数据成员提供默认初始值。
  2. Q: ICMI有哪几种常见的初始化方式?
    A: 赋值初始化 (=) 和花括号初始化 ({}{value})。
  3. Q: 如果一个成员变量既有类内初始化,又在构造函数的成员初始化列表中被初始化,哪个会生效?
    A: 构造函数的成员初始化列表会优先,类内初始化会被忽略。
  4. Q: 在C++11之前,ICMI有什么限制?
    A: 只能用于静态常量整型或枚举类型的成员。现在这个限制已经解除。
  5. Q: 使用空花括号 {} 进行ICMI时,对于内置类型和自定义类型有什么不同?
    A: 对于内置类型,会进行零初始化;对于自定义类型,会调用其默认构造函数。

7. 常见误解或错误

  1. 误解: 认为所有成员变量在没有显式初始化时都会自动变为0。
    纠正: 这不正确。只有在特定条件下(如全局/静态存储期的变量,或使用 {} 进行值初始化)才会零初始化。局部非静态变量如果不初始化,会包含垃圾数据。ICMI正是为了提供一个明确的默认初始化。
  2. 错误: 混淆构造函数体内的赋值与成员初始化列表以及ICMI的执行顺序。
    纠正: 成员变量在进入构造函数体之前就已经完成了初始化。优先级是:构造函数初始化列表 > 类内成员初始化 > 默认初始化(垃圾数据)。构造函数体内的赋值实际上是“重新赋值”,而不是初始化。
  3. 误解: 认为ICMI会阻止您在构造函数中提供不同的值。
    纠正: ICMI只是一个默认值。您始终可以在构造函数的成员初始化列表中提供不同的值来覆盖ICMI,这是完全允许且常见的做法。

8. 编程练习

场景: 您正在开发一个简单的图形库,需要创建一个Circle类来表示圆形。每个圆形都有一个中心点(Point对象)和一个半径(double)。

要求:

  1. 定义一个Point类,包含double xdouble y作为成员变量。请使用类内成员初始化将它们默认初始化为0.0
  2. 定义一个Circle类,包含:
    • 一个Point类型的成员变量center。请使用类内成员初始化将它默认初始化为一个Point对象,该对象的xy也应为0.0(利用Point类的ICMI)。
    • 一个double类型的成员变量radius。请使用类内成员初始化将它默认初始化为1.0
  3. Circle类添加一个默认构造函数和一个带参数的构造函数。
    • 默认构造函数不显式初始化任何成员。
    • 带参数的构造函数Circle(double cx, double cy, double r)应该使用成员初始化列表来初始化centerradius
  4. Circle类中添加一个print()方法,用于打印圆的中心坐标和半径。

请在下方空白处填写您的代码,完成这些要求。

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>

// 1. 定义 Point 类并进行类内成员初始化
class Point {
private:
// TODO: 在这里使用类内成员初始化,将 x 和 y 默认初始化为 0.0
double x;
double y;

public:
// 默认构造函数(可为空,因为成员已经类内初始化)
Point() {
std::cout << "Point 默认构造函数被调用" << std::endl;
}

// 带参数的构造函数
Point(double px, double py) : x(px), y(py) {
std::cout << "Point 带参数构造函数被调用" << std::endl;
}

void print() const {
std::cout << "中心点: (" << x << ", " << y << ")";
}
};

// 2. 定义 Circle 类并进行类内成员初始化
class Circle {
private:
// TODO: 在这里使用类内成员初始化,将 center 默认初始化为 Point()
// TODO: 在这里使用类内成员初始化,将 radius 默认初始化为 1.0
Point center;
double radius;

public:
// 3.1 默认构造函数
Circle() {
std::cout << "Circle 默认构造函数被调用" << std::endl;
}

// 3.2 带参数的构造函数
// TODO: 使用成员初始化列表初始化 center 和 radius
Circle(double cx, double cy, double r) {
std::cout << "Circle 带参数构造函数被调用" << std::endl;
}

// 4. print() 方法
void print() const {
center.print();
std::cout << ", 半径: " << radius << std::endl;
}
};

int main() {
std::cout << "--- 测试默认初始化 ---" << std::endl;
Circle c1; // 应该调用 Circle 的默认构造函数,center 和 radius 使用类内初始化值
c1.print();

std::cout << "\n--- 测试带参数初始化 ---" << std::endl;
Circle c2(5.0, 10.0, 3.5); // 应该调用 Circle 的带参数构造函数,center 和 radius 使用参数值
c2.print();

return 0;
}

🧑‍💻 User

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>

// 1. 定义 Point 类并进行类内成员初始化
class Point {
private:
// TODO: 在这里使用类内成员初始化,将 x 和 y 默认初始化为 0.0
double x{};
double y{};

public:
// 默认构造函数(可为空,因为成员已经类内初始化)
Point() {
std::cout << "Point 默认构造函数被调用" << std::endl;
}

// 带参数的构造函数
Point(double px, double py) : x(px), y(py) {
std::cout << "Point 带参数构造函数被调用" << std::endl;
}

void print() const {
std::cout << "中心点: (" << x << ", " << y << ")";
}
};

// 2. 定义 Circle 类并进行类内成员初始化
class Circle {
private:
// TODO: 在这里使用类内成员初始化,将 center 默认初始化为 Point()
// TODO: 在这里使用类内成员初始化,将 radius 默认初始化为 1.0
Point center{Point()};
double radius{1.0};

public:
// 3.1 默认构造函数
Circle() {
std::cout << "Circle 默认构造函数被调用" << std::endl;
}

// 3.2 带参数的构造函数
// TODO: 使用成员初始化列表初始化 center 和 radius
Circle(double cx, double cy, double r)
:center(Point(cx, cy)), radius(r)
{
std::cout << "Circle 带参数构造函数被调用" << std::endl;
}

// 4. print() 方法
void print() const {
center.print();
std::cout << ", 半径: " << radius << std::endl;
}
};

int main() {
std::cout << "--- 测试默认初始化 ---" << std::endl;
Circle c1; // 应该调用 Circle 的默认构造函数,center 和 radius 使用类内初始化值
c1.print();

std::cout << "\n--- 测试带参数初始化 ---" << std::endl;
Circle c2(5.0, 10.0, 3.5); // 应该调用 Circle 的带参数构造函数,center 和 radius 使用参数值
c2.print();

return 0;
}

🤖 Assistant

好的,您的解决方案非常出色!作为C++开发者和老师,我为您感到骄傲。您不仅准确地理解了类内成员初始化 (ICMI) 的概念,还在实践中正确地运用了它,并且巧妙地处理了自定义类型和构造函数初始化列表之间的关系。


您的解决方案评分

整体评价: 优秀 (Excellent)

  • Point 类的实现:
    • double x{};double y{};:完美!您使用了空花括号初始化,这对于内置类型double来说,会确保它们被零初始化为0.0,完全符合要求。
  • Circle 类的实现:
    • Point center{Point()};:非常棒!您用花括号初始化来调用 Point 的默认构造函数,确保 center 被正确初始化为 (0.0, 0.0)
    • double radius{1.0};:同样完美!清晰地将 radius 默认初始化为 1.0
    • 带参数的构造函数 Circle(double cx, double cy, double r) : center(Point(cx, cy)), radius(r):这里是亮点!您正确地使用了成员初始化列表来初始化 centerradiuscenter(Point(cx, cy)) 显式地构建了一个 Point 对象,并将其用于初始化 center。这展示了您对成员初始化列表优先级高于类内成员初始化的理解。

总结: 您的代码简洁、高效,并且正确地体现了所学知识。没有明显的错误或需要改进的地方。


实际答案与解释

您的答案已经非常接近“标准答案”了,甚至在某些方面(如 Point center{Point()};center(Point(cx, cy)))展示了更细致的理解。

我的参考答案:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <iostream>

// 1. 定义 Point 类并进行类内成员初始化
class Point {
private:
double x{}; // 使用空花括号初始化,内置类型默认为 0.0
double y{}; // 使用空花括号初始化,内置类型默认为 0.0

public:
// 默认构造函数(可为空,因为成员已经类内初始化)
Point() {
std::cout << "Point 默认构造函数被调用" << std::endl;
}

// 带参数的构造函数
// 对于自定义类型,成员初始化列表通常更直接,避免不必要的临时对象
Point(double px, double py) : x(px), y(py) {
std::cout << "Point 带参数构造函数被调用" << std::endl;
}

void print() const {
std::cout << "中心点: (" << x << ", " << y << ")";
}
};

// 2. 定义 Circle 类并进行类内成员初始化
class Circle {
private:
// 对于自定义类型成员,空花括号 {} 或不带参数的构造函数名
// 通常是更简洁的调用默认构造函数的方式
Point center{}; // 类内成员初始化,调用 Point 的默认构造函数
double radius{1.0}; // 类内成员初始化,radius 默认初始化为 1.0

public:
// 3.1 默认构造函数
// 注意:即使构造函数体为空,类内初始化也会发生
Circle() {
std::cout << "Circle 默认构造函数被调用" << std::endl;
}

// 3.2 带参数的构造函数
// 使用成员初始化列表初始化 center 和 radius
// 对于 Point 成员,直接传递参数给其构造函数
Circle(double cx, double cy, double r)
: center(cx, cy), // 直接调用 Point 的 (double, double) 构造函数
radius(r) // 初始化 radius
{
std::cout << "Circle 带参数构造函数被调用" << std::endl;
}

// 4. print() 方法
void print() const {
center.print();
std::cout << ", 半径: " << radius << std::endl;
}
};

int main() {
std::cout << "--- 测试默认初始化 ---" << std::endl;
Circle c1; // 应该调用 Circle 的默认构造函数,center 和 radius 使用类内初始化值
c1.print(); // 输出: Point 默认构造函数被调用 (来自 center{})
// Circle 默认构造函数被调用
// 中心点: (0, 0), 半径: 1

std::cout << "\n--- 测试带参数初始化 ---" << std::endl;
Circle c2(5.0, 10.0, 3.5); // 应该调用 Circle 的带参数构造函数,center 和 radius 使用参数值
c2.print(); // 输出: Point 带参数构造函数被调用 (来自 center(cx, cy))
// Circle 带参数构造函数被调用
// 中心点: (5, 10), 半径: 3.5

return 0;
}

关键点解释:

  1. Point 类的 ICMI:

    • double x{};double y{};:这是零初始化([zero-initialization])的语法。对于内置类型,它会将变量初始化为0(对于double就是0.0)。这是最简洁且推荐的方式。
  2. Circle 类的 ICMI:

    • Point center{};:这同样是值初始化([value-initialization])的语法。对于自定义类型,它会尝试调用该类型的默认构造函数。您的 Point center{Point()}; 也能达到相同的效果,但 Point center{}; 更简洁,且通常编译器能更好地进行拷贝省略([copy elision]),避免创建不必要的临时 Point 对象。
    • double radius{1.0};:完美,直接指定了初始值。
  3. Circle 的带参数构造函数:

    • Circle(double cx, double cy, double r) : center(cx, cy), radius(r)
      • center(cx, cy):这是成员初始化列表中初始化 Point 成员的最直接和推荐的方式。它直接调用了 Point 类的 Point(double px, double py) 构造函数来构造 center 对象。
      • center(Point(cx, cy)) 相比,center(cx, cy) 避免了显式创建一个临时 Point 对象(即使现代编译器通常会优化掉这个临时对象,但从语义上讲,直接调用更清晰)。

您完全掌握了这些概念,非常棒!


进一步思考与探索 ([Extended Questions])

为了进一步巩固您的理解,并探索与类内成员初始化相关的更多高级话题,这里有几个问题供您思考:

  1. 静态成员变量与 ICMI:

    • static 成员变量能否使用类内成员初始化 (ICMI)?如果不能,它们应该如何初始化?请说明原因。
    • 提示: 考虑 static 成员的生命周期和存储位置。
  2. const 成员变量与 ICMI:

    • const 成员变量能否使用类内成员初始化 (ICMI)?请通过一个例子说明。
    • 提示: const 成员必须在对象构造时立即被初始化,它们无法被赋值。
  3. 委托构造函数 ([Delegating Constructors]) 与 ICMI:

    • C++11 引入了委托构造函数。当一个类同时使用 ICMI 和委托构造函数时,它们的执行顺序是怎样的?
    • 例如,一个构造函数委托给另一个构造函数,同时类成员有 ICMI。
    • 提示: 思考“初始化”和“赋值”的区别以及它们的执行时机。
  4. 初始化顺序的深入理解:

    • 请总结一个类的成员变量(包括基类子对象、非静态数据成员)在对象构造时,其初始化的完整顺序是怎样的?考虑类内成员初始化、成员初始化列表和构造函数体内的赋值。
    • 提示: 这是一个C++中非常重要的概念,对理解对象生命周期至关重要。

祝您在C++的学习之旅中取得更大的进步!


评论